import AsyncValidator from 'async-validator'
import warning from 'warning'
import get from 'lodash/get'
import set from 'lodash/set'
import omit from 'lodash/omit'
import createFieldsStore from './createFieldsStore'
import { cloneElement } from '../../_util/vnode'
import BaseMixin from '../../_util/BaseMixin'
import { getOptionProps, getEvents } from '../../_util/props-util'
import PropTypes from '../../_util/vue-types'

import {
  argumentContainer,
  identity,
  normalizeValidateRules,
  getValidateTriggers,
  getValueFromEvent,
  hasRules,
  getParams,
  isEmptyObject,
  flattenArray,
} from './utils'

const DEFAULT_TRIGGER = 'change'

function createBaseForm (option = {}, mixins = []) {
  const {
    validateMessages,
    onFieldsChange,
    onValuesChange,
    mapProps = identity,
    mapPropsToFields,
    fieldNameProp,
    fieldMetaProp,
    fieldDataProp,
    formPropName = 'form',
    props = {},
    templateContext,
  } = option

  return function decorate (WrappedComponent) {
    let formProps = {}
    if (Array.isArray(props)) {
      props.forEach((prop) => {
        formProps[prop] = PropTypes.any
      })
    } else {
      formProps = props
    }
    const Form = {
      mixins: [BaseMixin, ...mixins],
      props: {
        ...formProps,
        wrappedComponentRef: PropTypes.func.def(() => {}),
      },
      data () {
        const fields = mapPropsToFields && mapPropsToFields(this.$props)
        this.fieldsStore = createFieldsStore(fields || {})

        this.instances = {}
        this.cachedBind = {}
        this.clearedFieldMetaCache = {};
        // HACK: https://github.com/ant-design/ant-design/issues/6406
        ['getFieldsValue',
          'getFieldValue',
          'setFieldsInitialValue',
          'getFieldsError',
          'getFieldError',
          'isFieldValidating',
          'isFieldsValidating',
          'isFieldsTouched',
          'isFieldTouched'].forEach(key => {
          this[key] = (...args) => {
            return this.fieldsStore[key](...args)
          }
        })

        return {
          submitting: false,
        }
      },
      watch: {
        '$props': {
          handler: function (nextProps) {
            if (mapPropsToFields) {
              this.fieldsStore.updateFields(mapPropsToFields(nextProps))
            }
          },
          deep: true,
        },
      },
      mounted () {
        this.wrappedComponentRef(this.$refs.WrappedComponent)
      },
      updated () {
        this.wrappedComponentRef(this.$refs.WrappedComponent)
      },
      destroyed () {
        this.wrappedComponentRef(null)
      },
      methods: {
        onCollectCommon (name, action, args) {
          const fieldMeta = this.fieldsStore.getFieldMeta(name)
          if (fieldMeta[action]) {
            fieldMeta[action](...args)
          } else if (fieldMeta.originalProps && fieldMeta.originalProps[action]) {
            fieldMeta.originalProps[action](...args)
          }
          const value = fieldMeta.getValueFromEvent
            ? fieldMeta.getValueFromEvent(...args)
            : getValueFromEvent(...args)
          if (onValuesChange && value !== this.fieldsStore.getFieldValue(name)) {
            const valuesAll = this.fieldsStore.getAllValues()
            const valuesAllSet = {}
            valuesAll[name] = value
            Object.keys(valuesAll).forEach(key => set(valuesAllSet, key, valuesAll[key]))
            onValuesChange(this, set({}, name, value), valuesAllSet)
          }
          const field = this.fieldsStore.getField(name)
          return ({ name, field: { ...field, value, touched: true }, fieldMeta })
        },

        onCollect (name_, action, ...args) {
          const { name, field, fieldMeta } = this.onCollectCommon(name_, action, args)
          const { validate } = fieldMeta
          const newField = {
            ...field,
            dirty: hasRules(validate),
          }
          this.setFields({
            [name]: newField,
          })
        },

        onCollectValidate (name_, action, ...args) {
          const { field, fieldMeta } = this.onCollectCommon(name_, action, args)
          const newField = {
            ...field,
            dirty: true,
          }
          this.validateFieldsInternal([newField], {
            action,
            options: {
              firstFields: !!fieldMeta.validateFirst,
            },
          })
        },

        getCacheBind (name, action, fn) {
          if (!this.cachedBind[name]) {
            this.cachedBind[name] = {}
          }
          const cache = this.cachedBind[name]
          if (!cache[action]) {
            cache[action] = fn.bind(this, name, action)
          }
          return cache[action]
        },

        recoverClearedField (name) {
          if (this.clearedFieldMetaCache[name]) {
            this.fieldsStore.setFields({
              [name]: this.clearedFieldMetaCache[name].field,
            })
            this.fieldsStore.setFieldMeta(name, this.clearedFieldMetaCache[name].meta)
            delete this.clearedFieldMetaCache[name]
          }
        },

        getFieldDecorator (name, fieldOption) {
          const { props, ...restProps } = this.getFieldProps(name, fieldOption)
          return (fieldElem) => {
            const fieldMeta = this.fieldsStore.getFieldMeta(name)
            const originalProps = getOptionProps(fieldElem)
            const originalEvents = getEvents(fieldElem)
            if (process.env.NODE_ENV !== 'production') {
              const valuePropName = fieldMeta.valuePropName
              warning(
                !(valuePropName in originalProps),
                `\`getFieldDecorator\` will override \`${valuePropName}\`, ` +
                `so please don't set \`${valuePropName} and v-model\` directly ` +
                `and use \`setFieldsValue\` to set it.`
              )
              const defaultValuePropName =
                `default${valuePropName[0].toUpperCase()}${valuePropName.slice(1)}`
              warning(
                !(defaultValuePropName in originalProps),
                `\`${defaultValuePropName}\` is invalid ` +
                `for \`getFieldDecorator\` will set \`${valuePropName}\`,` +
                ` please use \`option.initialValue\` instead.`
              )
            }
            fieldMeta.originalProps = originalProps
            // fieldMeta.ref = fieldElem.data && fieldElem.data.ref
            const newProps = {
              props: {
                ...props,
                ...this.fieldsStore.getFieldValuePropValue(fieldMeta),
              },
              ...restProps,
            }
            newProps.domProps.value = newProps.props.value
            const newEvents = {}
            Object.keys(newProps.on).forEach((key) => {
              if (originalEvents[key]) {
                const triggerEvents = newProps.on[key]
                newEvents[key] = (...args) => {
                  originalEvents[key](...args)
                  triggerEvents(...args)
                }
              } else {
                newEvents[key] = newProps.on[key]
              }
            })
            return cloneElement(fieldElem, { ...newProps, on: newEvents })
          }
        },

        getFieldProps (name, usersFieldOption = {}) {
          if (!name) {
            throw new Error('Must call `getFieldProps` with valid name string!')
          }
          if (process.env.NODE_ENV !== 'production') {
            warning(
              this.fieldsStore.isValidNestedFieldName(name),
              'One field name cannot be part of another, e.g. `a` and `a.b`.'
            )
            warning(
              !('exclusive' in usersFieldOption),
              '`option.exclusive` of `getFieldProps`|`getFieldDecorator` had been remove.'
            )
          }

          delete this.clearedFieldMetaCache[name]

          const fieldOption = {
            name,
            trigger: DEFAULT_TRIGGER,
            valuePropName: 'value',
            validate: [],
            ...usersFieldOption,
          }

          const {
            rules,
            trigger,
            validateTrigger = trigger,
            validate,
          } = fieldOption

          const fieldMeta = this.fieldsStore.getFieldMeta(name)
          if ('initialValue' in fieldOption) {
            fieldMeta.initialValue = fieldOption.initialValue
          }

          const inputProps = {
            ...this.fieldsStore.getFieldValuePropValue(fieldOption),
            // ref: name,
          }
          const inputListeners = {}
          const inputAttrs = {}
          if (fieldNameProp) {
            inputProps[fieldNameProp] = name
          }

          const validateRules = normalizeValidateRules(validate, rules, validateTrigger)
          const validateTriggers = getValidateTriggers(validateRules)
          validateTriggers.forEach((action) => {
            if (inputListeners[action]) return
            inputListeners[action] = this.getCacheBind(name, action, this.onCollectValidate)
          })

          // make sure that the value will be collect
          if (trigger && validateTriggers.indexOf(trigger) === -1) {
            inputListeners[trigger] = this.getCacheBind(name, trigger, this.onCollect)
          }

          const meta = {
            ...fieldMeta,
            ...fieldOption,
            validate: validateRules,
          }
          this.fieldsStore.setFieldMeta(name, meta)
          if (fieldMetaProp) {
            inputAttrs[fieldMetaProp] = meta
          }

          if (fieldDataProp) {
            inputAttrs[fieldDataProp] = this.fieldsStore.getField(name)
          }

          return {
            props: omit(inputProps, ['id']),
            // id: inputProps.id,
            domProps: {
              value: inputProps.value,
            },
            attrs: {
              ...inputAttrs,
              id: inputProps.id,
            },
            directives: [
              {
                name: 'ant-ref',
                value: this.getCacheBind(name, `${name}__ref`, this.saveRef),
              },
            ],
            on: inputListeners,
          }
        },

        getFieldInstance (name) {
          return this.instances[name]
        },

        getRules (fieldMeta, action) {
          const actionRules = fieldMeta.validate.filter((item) => {
            return !action || item.trigger.indexOf(action) >= 0
          }).map((item) => item.rules)
          return flattenArray(actionRules)
        },

        setFields (maybeNestedFields, callback) {
          const fields = this.fieldsStore.flattenRegisteredFields(maybeNestedFields)
          this.fieldsStore.setFields(fields)
          if (onFieldsChange) {
            const changedFields = Object.keys(fields)
              .reduce((acc, name) => set(acc, name, this.fieldsStore.getField(name)), {})
            onFieldsChange(this, changedFields, this.fieldsStore.getNestedAllFields())
          }
          if (templateContext) {
            templateContext.$forceUpdate()
          } else {
            this.$forceUpdate()
          }
          this.$nextTick(() => {
            callback && callback()
          })
        },

        resetFields (ns) {
          const newFields = this.fieldsStore.resetFields(ns)
          if (Object.keys(newFields).length > 0) {
            this.setFields(newFields)
          }
          if (ns) {
            const names = Array.isArray(ns) ? ns : [ns]
            names.forEach(name => delete this.clearedFieldMetaCache[name])
          } else {
            this.clearedFieldMetaCache = {}
          }
        },

        setFieldsValue (changedValues, callback) {
          const { fieldsMeta } = this.fieldsStore
          const values = this.fieldsStore.flattenRegisteredFields(changedValues)
          const newFields = Object.keys(values).reduce((acc, name) => {
            const isRegistered = fieldsMeta[name]
            if (process.env.NODE_ENV !== 'production') {
              warning(
                isRegistered,
                'Cannot use `setFieldsValue` until ' +
                  'you use `getFieldDecorator` or `getFieldProps` to register it.'
              )
            }
            if (isRegistered) {
              const value = values[name]
              acc[name] = {
                value,
              }
            }
            return acc
          }, {})
          this.setFields(newFields, callback)
          if (onValuesChange) {
            const allValues = this.fieldsStore.getAllValues()
            onValuesChange(this, changedValues, allValues)
          }
        },

        saveRef (name, _, component) {
          if (!component) {
            // after destroy, delete data
            this.clearedFieldMetaCache[name] = {
              field: this.fieldsStore.getField(name),
              meta: this.fieldsStore.getFieldMeta(name),
            }
            this.fieldsStore.clearField(name)
            delete this.instances[name]
            delete this.cachedBind[name]
            return
          }
          this.recoverClearedField(name)
          // const fieldMeta = this.fieldsStore.getFieldMeta(name)
          // if (fieldMeta) {
          //   const ref = fieldMeta.ref
          //   if (ref) {
          //     if (typeof ref === 'string') {
          //       throw new Error(`can not set ref string for ${name}`)
          //     }
          //     ref(component)
          //   }
          // }
          this.instances[name] = component
        },

        validateFieldsInternal (fields, {
          fieldNames,
          action,
          options = {},
        }, callback) {
          const allRules = {}
          const allValues = {}
          const allFields = {}
          const alreadyErrors = {}
          fields.forEach((field) => {
            const name = field.name
            if (options.force !== true && field.dirty === false) {
              if (field.errors) {
                set(alreadyErrors, name, { errors: field.errors })
              }
              return
            }
            const fieldMeta = this.fieldsStore.getFieldMeta(name)
            const newField = {
              ...field,
            }
            newField.errors = undefined
            newField.validating = true
            newField.dirty = true
            allRules[name] = this.getRules(fieldMeta, action)
            allValues[name] = newField.value
            allFields[name] = newField
          })
          this.setFields(allFields)
          // in case normalize
          Object.keys(allValues).forEach((f) => {
            allValues[f] = this.fieldsStore.getFieldValue(f)
          })
          if (callback && isEmptyObject(allFields)) {
            callback(isEmptyObject(alreadyErrors) ? null : alreadyErrors,
              this.fieldsStore.getFieldsValue(fieldNames))
            return
          }
          const validator = new AsyncValidator(allRules)
          if (validateMessages) {
            validator.messages(validateMessages)
          }
          validator.validate(allValues, options, (errors) => {
            const errorsGroup = {
              ...alreadyErrors,
            }
            if (errors && errors.length) {
              errors.forEach((e) => {
                const fieldName = e.field
                const field = get(errorsGroup, fieldName)
                if (typeof field !== 'object' || Array.isArray(field)) {
                  set(errorsGroup, fieldName, { errors: [] })
                }
                const fieldErrors = get(errorsGroup, fieldName.concat('.errors'))
                fieldErrors.push(e)
              })
            }
            const expired = []
            const nowAllFields = {}
            Object.keys(allRules).forEach((name) => {
              const fieldErrors = get(errorsGroup, name)
              const nowField = this.fieldsStore.getField(name)
              // avoid concurrency problems
              if (nowField.value !== allValues[name]) {
                expired.push({
                  name,
                })
              } else {
                nowField.errors = fieldErrors && fieldErrors.errors
                nowField.value = allValues[name]
                nowField.validating = false
                nowField.dirty = false
                nowAllFields[name] = nowField
              }
            })
            this.setFields(nowAllFields)
            if (callback) {
              if (expired.length) {
                expired.forEach(({ name }) => {
                  const fieldErrors = [{
                    message: `${name} need to revalidate`,
                    field: name,
                  }]
                  set(errorsGroup, name, {
                    expired: true,
                    errors: fieldErrors,
                  })
                })
              }

              callback(isEmptyObject(errorsGroup) ? null : errorsGroup,
                this.fieldsStore.getFieldsValue(fieldNames))
            }
          })
        },

        validateFields (ns, opt, cb) {
          const { names, callback, options } = getParams(ns, opt, cb)
          const fieldNames = names
            ? this.fieldsStore.getValidFieldsFullName(names)
            : this.fieldsStore.getValidFieldsName()
          const fields = fieldNames
            .filter(name => {
              const fieldMeta = this.fieldsStore.getFieldMeta(name)
              return hasRules(fieldMeta.validate)
            }).map((name) => {
              const field = this.fieldsStore.getField(name)
              field.value = this.fieldsStore.getFieldValue(name)
              return field
            })
          if (!fields.length) {
            if (callback) {
              callback(null, this.fieldsStore.getFieldsValue(fieldNames))
            }
            return
          }
          if (!('firstFields' in options)) {
            options.firstFields = fieldNames.filter((name) => {
              const fieldMeta = this.fieldsStore.getFieldMeta(name)
              return !!fieldMeta.validateFirst
            })
          }
          this.validateFieldsInternal(fields, {
            fieldNames,
            options,
          }, callback)
        },

        isSubmitting () {
          if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
            warning(
              false,
              '`isSubmitting` is deprecated. ' +
                'Actually, it\'s more convenient to handle submitting status by yourself.'
            )
          }
          return this.submitting
        },

        submit (callback) {
          if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
            warning(
              false,
              '`submit` is deprecated.' +
                'Actually, it\'s more convenient to handle submitting status by yourself.'
            )
          }
          const fn = () => {
            this.setState({
              submitting: false,
            })
          }
          this.setState({
            submitting: true,
          })
          callback(fn)
        },
      },

      render () {
        const { $listeners, $slots } = this
        const formProps = {
          [formPropName]: this.getForm(),
        }
        const props = getOptionProps(this)
        const wrappedComponentProps = {
          props: mapProps.call(this, {
            ...formProps,
            ...props,
          }),
          on: $listeners,
          ref: 'WrappedComponent',
        }
        return <WrappedComponent {...wrappedComponentProps}>{$slots.default}</WrappedComponent>
      },
    }
    if (Array.isArray(WrappedComponent.props)) {
      const newProps = {}
      WrappedComponent.props.forEach((prop) => {
        newProps[prop] = PropTypes.any
      })
      newProps[formPropName] = Object
      WrappedComponent.props = newProps
    } else {
      WrappedComponent.props = WrappedComponent.props || {}
      if (!(formPropName in WrappedComponent.props)) {
        WrappedComponent.props[formPropName] = Object
      }
    }
    return argumentContainer(Form, WrappedComponent)
  }
}

export default createBaseForm
